package inglar.autotc.client.ui;

import inglar.autotc.client.HasContainingTreeItem;
import inglar.autotc.client.NodeLookup;
import inglar.autotc.client.resource.ResourceHandler;
import inglar.autotc.shared.Values;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.TreeItem;
import com.google.gwt.user.client.ui.VerticalPanel;

/**
 * A renderable representation of a tc class object. 
 * @author Lee
 */
public class NodeClass extends Composite implements HasContainingTreeItem {

//	private String script = "initscript";
	
	private String classid = "initclassid";
	private String parent = "initparent";
	
	private String classname = "initclass";
	private String options = "initopts opts";
	
	private String device = "initdevice";
	private String action = "initaction";
	
	private HorizontalPanel contentPanel;
	private Image qicon;
	private HTML lblClassid;
	private HTML lblParent;
	private HTML lblClassname;
	
	private VerticalPanel varopts;
	private HorizontalPanel buttons;
	
	public Button btnDelete;
	public Button btnModify;
	public Button btnAddQDisc;
	public Button btnAddClass;
	public Button btnAddFilter;
	
	PageDevices tsRef = null;
	NodeClass self = null;
	private TreeItem container = null;
	
	public boolean isPseudoClass = false;
	
	/**
	 * @wbp.parser.constructor
	 */
	public NodeClass() {
		contentPanel = new HorizontalPanel();
		initWidget(contentPanel);
		contentPanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
		contentPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
		
		qicon = new Image(ResourceHandler.INSTANCE.cnavy16());
		contentPanel.add(qicon);
		qicon.getElement().setClassName("nodeIcon");
		
		VerticalPanel p1 = new VerticalPanel();
		contentPanel.add(p1);
		p1.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_LEFT);
		p1.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
		
		lblClassname = new HTML();
		p1.add(lblClassname);
		lblClassname.getElement().setClassName("tsNodeLabel");
		
		lblClassid = new HTML();
		p1.add(lblClassid);
		lblClassid.getElement().setClassName("tsNodeLabel");
		
		lblParent = new HTML();
		p1.add(lblParent);
		lblParent.getElement().setClassName("tsNodeLabel");
		
		varopts = new VerticalPanel();
		contentPanel.add(varopts);
		varopts.getElement().setAttribute("style", "margin-left: 15px;");
		
		buttons = new HorizontalPanel();
		contentPanel.add(buttons);
		buttons.getElement().setAttribute("style", "margin-left: 15px;");
		
		VerticalPanel p2 = new VerticalPanel();
		buttons.add(p2);
		
		btnDelete = new Button("DELETE");
		p2.add(btnDelete);
		btnDelete.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
					container.remove();
			}
		});
		btnDelete.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small;");
		
		btnModify = new Button("MODIFY");
		p2.add(btnModify);
		btnModify.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				DialogBoxEditNodeClass nce = new DialogBoxEditNodeClass();
				nce.txtbxParent.setEnabled(false);
//				nce.txtbxClassid.setEnabled(false);
				nce.setTargetNode(self);
				nce.center();
				if(container.getChildCount() > 0) nce.txtbxClassid.setEnabled(false);
			}
		});
		btnModify.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small;");
		
		VerticalPanel p3 = new VerticalPanel();
		buttons.add(p3);
		
		btnAddQDisc = new Button("ADD QDISC");
		p3.add(btnAddQDisc);
		btnAddQDisc.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				if(container.getChildCount() > 0) return;
				String minorid = NodeLookup.getMinor(getClassid());
				String device = getDevice();
				String tc = "tc qdisc add dev " + device + " parent " + getClassid() + " handle " + minorid + ": pfifo_fast";
				NodeQDisc nq = new NodeQDisc(tc, tsRef);
				TreeItem ti = container.addItem(nq);
				nq.setContainer(ti);
				container.setState(true);
			}
		});
		btnAddQDisc.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small; width: 80px;");
		
		btnAddClass = new Button("ADD CLASS");
		p3.add(btnAddClass);
		btnAddClass.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				if(getClassname().equals("htb")) {
					int children = container.getChildCount();
					int maxm = 0;
					for(int i = 0; i < children; i++) {
						Object o = container.getChild(i).getWidget();
						if(!(o instanceof NodeClass)) return;
						NodeClass nc = (NodeClass) o;
						String minor = NodeLookup.getMinor(nc.getClassid());
						int m = Integer.parseInt(minor, 16);
						if(m > maxm) maxm = m;
					}
					String major = NodeLookup.getMinor(getClassid());
					if(major.equals("0")) major = "1";
					String classid = major + ":" + Integer.toHexString(maxm + 1);
					String device = getDevice();
					String handle = getClassid();
					String tc =  "tc class add dev " + device + " parent " + handle + " classid " + classid + " htb rate 1bps";
					NodeClass nc = new NodeClass(tc, tsRef);
					TreeItem c = container.addItem(nc);
					nc.setContainer(c);
					container.setState(true);
				} else {
					Window.alert("Adding classes to this qdisc is not supported.");
				}
			}
		});
		btnAddClass.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small; width: 80px;");
		
		btnAddFilter = new Button("ADD FILTER");
		buttons.add(btnAddFilter);
		btnAddFilter.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				NodeFilter nf = new NodeFilter("tc filter add dev " + device + " pref 1 protocol ip parent " + classid + " flowid " + classid);
				nf.setContainer(container.addItem(nf));
				container.setState(true);
			}
		});
		btnAddFilter.getElement().setAttribute("style", "font-weight: bold; font-size: xx-small;");
		
		this.getElement().setClassName("tsNodeClass");
		
		self = this;
	}

	public NodeClass(String tcCmd, PageDevices ref) {
		this();
		if(ref == null) throw new IllegalArgumentException("Argument ref must not be null");
		this.setScript(tcCmd);
		this.tsRef = ref;
	}

	/**
	 * @return the action
	 */
	public String getAction() {
		return action;
	}

	/**
	 * @param action the action to set
	 */
	public void setAction(String action) {
		this.action = action;
	}

	/**
	 * @return the device
	 */
	public String getDevice() {
		return device;
	}

	/**
	 * @param device the device to set
	 */
	public void setDevice(String device) {
		this.device = device;
	}

	/**
	 * @return the options
	 */
	public String getOptions() {
		return options;
	}

	/**
	 * @param options the options to set
	 */
	public void setOptions(String str) {
		this.options = str;
		if(str.isEmpty()) return;
		varopts.clear();
		String[] opts = str.split(Values.REGEX_ALL_WHITESPACE); 
		for(int i = 0; i < opts.length; i += 2) {
			String temp = opts[i] + " <b>" + opts[i + 1] + "</b>";
			HTML html = new HTML(temp);
			varopts.add(html);
			html.getElement().setClassName("tsNodeLabel");
		}
	}

	/**
	 * @return the classname
	 */
	public String getClassname() {
		return classname;
	}

	/**
	 * @param classname the classname to set
	 */
	public void setClassname(String classname) {
		if(classname.equals("pseudo_prio")) {
			lblClassname.setHTML("<i>prio pseudoclass</i>");
			return;
		}
		if(!isClass(classname)) throw new IllegalArgumentException("Argument class (" + classname + ") is not a supported class");
		else {
			this.classname = classname;
			lblClassname.setHTML("class <b>" + classname + "</b>");
		}
	}

	/**
	 * @return the parent
	 */
	public String getParentId() {
		return parent;
	}

	/**
	 * @param parent the parent to set
	 */
	public void setParentId(String parentId) {
		this.parent = parentId;
		if(parentId.equals("root")) lblParent.setHTML("at <b>root</b>");
		else lblParent.setHTML("parent <b>" + getParentId() + "</b>");
	}

	/**
	 * @return the classid
	 */
	public String getClassid() {
		return classid;
	}

	/**
	 * @param classid the classid to set
	 */
	public void setClassid(String classid) {
		this.classid = classid;
		lblClassid.setHTML("classid <b>" + getClassid() + "</b>");
	}

	/**
	 * @return the script
	 */
	public String getScript() {
		if(isPseudoClass) return "";
		String tc = "";
		tc += "tc class add dev " + getDevice() + " parent " + getParentId() + " classid " + getClassid() + " " + getClassname() + " " + getOptions(); 
		return tc;
	}

	/**
	 * @param script the script to set
	 */
	public void setScript(String str) {
		String script = str.trim();

		String[] v = script.split(Values.REGEX_ALL_WHITESPACE);
		int i = 0;
		if(!v[i++].equals("tc")) throw new IllegalArgumentException("Argument script not a tc command");
		if(!v[i++].equals("class")) throw new IllegalArgumentException("Argument script not a tc class command");
		
		if(!v[i].equals("dev")) {
			String temp = v[i++];
			if(temp.equals("add")
//					|| temp.equals("del") || temp.equals("replace") || temp.equals("change") || temp.equals("show")
					) this.setAction(temp);
			else throw new IllegalArgumentException("Argument script has an invalid or unwanted optional parameter (" + temp + ") before dev");
		}
		
		if(!v[i++].equals("dev")) throw new IllegalArgumentException("Argument script does not have a dev parameter where expected");
		if(v[i].equals("classid") || v[i].equals("root") || v[i].equals("ingress") || v[i].equals("parent")) throw new IllegalArgumentException("Argument script has unsafe dev parameter value");
		else this.setDevice(v[i++]);
		
		int posParent = -1, posClassid = -1; 
		boolean getParent = false, getClassid = false;
		for( ; i < v.length; i++) {
			if(v[i].equals("root")) {
				if(posParent < 0) {
					if(getParent || getClassid) throw new IllegalArgumentException("Argument script has unexpected token ('root')");
					this.setParentId(v[i]);
					posParent = i;
				} else throw new IllegalArgumentException("Argument script already has parent or root attachment");
			} else if(v[i].equals("parent")) {
				if(posParent < 0) {
					if(getParent || getClassid) throw new IllegalArgumentException("Argument script has unexpected token ('parent')");
					getParent = true;
					posParent = i;
				} else throw new IllegalArgumentException("Argument script already has parent or root attachment");
			} else if(v[i].equals("classid")) {
				if(posClassid < 0) {
					if(getParent || getClassid) throw new IllegalArgumentException("Argument script has unexpected token ('classid')");
					getClassid = true;
					posClassid = i;
				} else throw new IllegalArgumentException("Argument script already has classid");
			} else {
				if(getParent) {
					this.setParentId(v[i]);
					getParent = false;
				} else if(getClassid) {
					this.setClassid(v[i]);
					getClassid = false;
				} else break; // i must be pointing to QDISC_KIND
			}
		}
		
		// here, i must be pointing to QDISC_KIND
		this.setClassname(v[i++]); // already throws IAE if QDISC_KIND is unknown
		
		// XXX NodeClass.setScript doesn't care for OPTIONS
		String opts = "";
		for(int j = i; j < v.length; j++) opts += v[j] + " ";
		this.setOptions(opts.trim());
//		this.script = script;
	}

	/**
	 * List of tc classes supported by the application.
	 */
	public static String[] classes = {
//			"hfsc",
//			"cbq",
			"htb"
		};
	
	/**
	 * Determines whether a given string is a class supported by the application.
	 * @param str class
	 * @return <b>true</b> when parameter is a supported class; <b>false</b> otherwise
	 */
	public boolean isClass(String str) {
		for(int i = 0; i < classes.length; i++) {
			if(classes[i].equals(str)) return true;
		}
		return false;
	}

	/**
	 * @return the container
	 */
	public TreeItem getContainer() {
		return container;
	}

	/**
	 * @param container the container to set
	 */
	public void setContainer(TreeItem container) {
		this.container = container;
		container.setState(true);
	}
	
	public static NodeClass getPseudoClassPrio(String device, String priohandle, String minorid, PageDevices tsRef) {
		NodeClass retval = new NodeClass();
		retval.setClassname("pseudo_prio");
		String major = NodeLookup.getMajor(priohandle);
		String classid = major + ":" + minorid.trim();
		retval.setDevice(device);
		retval.setClassid(classid);
		retval.setParentId(priohandle);
		retval.isPseudoClass = true;
		retval.tsRef = tsRef;
		return retval;
	}
	
}

/*
# tc class help
Usage: tc class [ add | del | change | replace | show ] dev STRING
       [ classid CLASSID ] [ root | parent CLASSID ]
       [ [ QDISC_KIND ] [ help | OPTIONS ] ] ---------------------------------- will handle htb only, tc considers prio as classless (even though it creates
		                                                                           child classes automatically when a prio qdisc is attached), cbq rules are 
		                                                                           inhumane, and hfsc is hard to understand

       tc class show [ dev STRING ] [ root | parent CLASSID ]
Where:
QDISC_KIND := { prio | cbq | etc. }
OPTIONS := ... try tc class add <desired QDISC_KIND> help
#
*/